Gestern haben wir uns den Grundlagen der regulären Ausdrücke (regular expressions)
gewidmet. Sie haben die wichtigsten Metazeichen kennengelernt und wissen jetzt, wie
man sie verwendet, um Muster in Strings zu finden. Heute, im zweiten Teil unserer
regex
-Saga, bauen wir auf diesem Wissen auf und untersuchen etwas komplexere
Möglichkeiten für den Einsatz regulärer Ausdrücke. In diesem Kapitel zeige ich Ihnen,
split
-Funktion sonst noch einsetzen kann und
Mit Hilfe von Mustern im Booleschen skalaren Kontext einer if
- oder einer
Schleifenbedingung können Sie feststellen, ob Ihr Muster mit einem Teil eines Strings
übereinstimmt oder nicht. Auf diese Frage gibt es jedoch nur zwei Antworten: Ja oder
Nein. Das mag zwar zur Überprüfung der Eingabe oder zum Ermitteln mehrerer
Vorkommen eines Musters in einem String ganz nützlich sein, ist aber nur eine Seite
der Medaille. Die Antworten Ja oder Nein sind zwar recht zweckmäßig, aber noch
nützlicher ist es, wenn man herausfinden kann, welche Daten genau dem Muster
entsprechen, um dann diese Daten später in dem Muster wiederzuverwenden oder
eine Liste aller gefundenen Übereinstimmungen aufzubauen.
Ob das Vorkommen, das Sie mit einem Muster finden, nutzbringend ist oder nicht,
hängt natürlich vom Muster ab. Lautet Ihr Muster /abc/
, dann werden auch die
gefundenen Daten abc
lauten, was Ihnen natürlich bereits vorher bekannt war. Wenn
jedoch Ihr Muster ungefähr so aussieht /\+.*/
(suche ein +
dann eine beliebige Anzahl
von Zeichen), kann Ihre Übereinstimmung aus einem beliebigen Satz von Zeichen
bestehen, die zufällig nach einem +
-Zeichen stehen. Dass man auf die gefundenen
Vorkommen zugreifen kann, ist eine ganz wichtige Eigenschaft der regulären
Ausdrücke.
In Perl gibt es mehrere Wege, um auf Fundstellen zuzugreifen, und verschiedene Möglichkeiten, diese - hat man sie erst einmal gefunden - zu nutzen. Gibt es eine Übereinstimmung, können Sie weiter hinten in dem gleichen Muster darauf Bezug nehmen, Sie können die Übereinstimmung in einer skalaren Variablen speichern oder eine Liste aller Übereinstimmungen anlegen. In diesem Abschnitt werden wir diese Möglichkeiten der Reihe nach besprechen.
Gestern habe ich Ihnen gezeigt, wie Sie mit Klammern Teile des Musters zusammenfassen können oder wie sich damit die Prioritäten beim Mustervergleich ändern lassen. Der dritte und vielleicht wichtigste Einsatzbereich von Klammern ist, die gefundene Übereinstimmung zu speichern, um dann weiter hinten zum Aufbau eines komplexeren regulären Ausdrucks auf diese Übereinstimmung Bezug zu nehmen. Dieser Mechanismus, Übereinstimmungen zu sichern, wird in der Sprache der regulären Ausdrücke oft auch als Rückbezug bezeichnet.
Betrachten wir dazu folgendes Beispiel. Angenommen wir suchen Zeilen, die mit dem
gleichen Wort beginnen und enden. Dabei ist es unerheblich, welches Wort das ist,
solange es am Anfang und am Ende der Zeile gleich lautet. Sie könnten dazu zwei
Mustertests, eine Schleife und mehrere if
-Anweisungen aufsetzen. Besser wäre es
jedoch, zu Beginn der Zeile das erste Wort abzufragen, den Wert zu speichern und
dann zu schauen, ob dieses Wort auch am Ende der Zeile vorkommt. Und so sähe ein
derartiger Ausdruck aus:
/^(\S+)\s.*\1$/
Ich möchte Ihnen diesen Ausdruck Zeichen für Zeichen aufschlüsseln. Das erste
Zeichen ist ein Caret (^
) und bezieht sich auf den Anfang der Zeile. Direkt daran
schließt sich eine Klammer an, die ein Muster einschließt, das für später gespeichert
werden soll. \S
ist ein beliebiges Nicht-Whitespace-Zeichen, und \S+
bedeutet ein oder
mehrere beliebige Nicht-Whitespace-Zeichen. Die schließende Klammer beendet den
Teil, der gespeichert werden soll. Können Sie mir noch folgen? Das Muster innerhalb
der Klammer schaut am Anfang der Zeile nach einer Reihe von Zeichen, gefolgt von
einem Whitespace-Zeichen (Leerzeichen, Tabulator etc.).
Doch fahren wir fort. Anschließend haben wir ein einziges Whitespace-Zeichen (\s
),
kein oder mehrere Zeichen beliebiger Art (.*
), \1
und dann das Zeichen für Ende der
Zeile ($
). Doch wozu der Ausdruck \1
? Dieser Code ist eine Referenz auf das, was wir
als Übereinstimmung in der Klammer gefunden haben. \1
besagt: »Setze das, was du
im ersten Klammerpaar gefunden hast, hierher.« Damit wird genau das, was wir
innerhalb dieser Klammer gefunden haben, später in dem Muster selbst erscheinen.
Damit das gesamte Muster wahr zurückliefert, muss die referenzierte
Übereinstimmung also auch am Ende der Zeile erscheinen. Alle Teile des Musters
müssen gefunden werden, nicht nur der Klammerinhalt.
Angenommen, Sie haben folgende Zeile:
»Perl ist zur schnellen Skripterstellung am besten geeignet.
Wenn Sie Ihre Arbeit gut machen wollen, wählen Sie Perl.«
(Gut, ich gebe zu, das sind zwei Zeilen. Nehmen Sie jedoch einfach an, es wäre eine
einzige Zeile innerhalb der Variablen $_
). Wird das obige Muster, angewendet auf
diese Zeile, eine Übereinstimmung (gleiches Wort am Anfang und am Ende der Zeile)
finden? Schauen wir mal. Das Muster sucht zuerst den Zeilenanfang, dann alle Nicht-
Whitespace-Zeichen bis zum ersten einfachen Whitespace-Zeichen. Das Wort »Perl«
und das anschließende Leerzeichen stimmen mit dem Muster überein. (Beachten Sie,
dass das Whitespace-Zeichen außerhalb der Klammer steht und somit nicht
mitgespeichert wird.) Der nächste Teil des Musters (.*
) schluckt alle Zeichen bis zum
Ende der Zeile. Hiernach halten wir Ausschau nach einem Leerzeichen und einem
weiteren Vorkommen der zuvor gefundenen Übereinstimmung, die wir am Anfang
der Zeile gespeichert haben. Denken Sie daran, hinter \1
verbirgt sich die am Anfang
gefundene Übereinstimmung, so dass wir nach dem Wort Perl
suchen. Nachdem wir
Perl
gefunden haben, erwartet das Muster mit $
noch das Ende der Zeile - aber nein,
was ist das? Die Zeile ist ja noch gar nicht zu Ende! Da steht ja noch ein Punkt! Und
wegen diesem Punkt liefert das gesamte Muster falsch zurück.
Wir können das jedoch problemlos beheben. Durch folgende Änderung am Muster kann man die Interpunktion berücksichtigen:
/^(\S+)\s.*\s\1[.!?"]$/
Am wichtigsten ist jedoch dabei die Tatsache, dass sich \1
auf das bezieht, was durch
das in Klammern stehende Muster gefunden wurde. Das ganze Muster ist nur
erfolgreich, wenn der ermittelte Klammerinhalt noch einmal dort im String auftaucht,
wo es durch die Referenz \1
vorgegeben wird.
Vielleicht wundern Sie sich, warum es gerade \1
heißt. Die Bezeichnung \1
resultiert
daraus, dass es nur eine Klammer gibt, in der ein Muster gespeichert wird. Möglich
sind durchaus mehrere gespeicherte Übereinstimmungen in einem Muster, die dann
jeweils von Klammern eingeschlossen sind und mit den Zahlen \1
, \2
, \3
und so
weiter angesprochen werden (gezählt wird von links nach rechts). Die Zahlen werden
jeweils den öffnenden Klammern zugewiesen - was bedeutet, dass Sie eine Klammer
nicht schließen müssen, bevor Sie eine neue öffnen. Sie können somit die Muster, für
die Sie eine Übereinstimmung suchen, verschachteln und mit der Zahl dann auf die
jeweilige Übereinstimmung Bezug nehmen.
Seien Sie jedoch vorsichtig mit dem Einsatz von Klammern - unabhängig davon, ob
Sie damit Gruppen definieren, um die Prioritäten zu ändern, oder
Übereinstimmungen sichern wollen - Perl wird auf alle Fälle die Werte speichern. Sie
können dies verhindern, indem Sie eine besondere Form der Klammer verwenden:
(?:muster
) anstelle von (muster
). Mehr dazu erfahren Sie im Abschnitt »Vertiefung« zu
diesem Kapitel.
Rückbezüge ermöglichen es Ihnen, eine für einen Teil des Musters gefundene
Übereinstimmung im Muster selbst wieder zu referenzieren. Sie können auf den
bereits gefundenen Teil eines Musters, aber auch von außerhalb des Musters
zugreifen. Zusätzlich zu der Numerierung der Rückbezüge durch \1
, \2
und so weiter
weist Perl den Werten der Teilfundstellen Skalarvariablen $1
, $2
und so weiter zu.
Dieser Mechanismus erweist sich als besonders praktisch, wenn man Teile aus einem
String oder anderen Daten extrahieren möchte. Sehen Sie dazu folgendes Beispiel,
bei dem das erste Wort aus einem String herausgelöst wird:
if (/^(\S+)\s/) {
print "Erstes Wort: $1\n";
}
Hierbei wird Perl, wenn es eine Entsprechung zu dem Muster findet (das heißt, wenn
es einen Zeilenanfang gefolgt von einem oder mehreren Nicht-Whitespace-Zeichen
und einem Whitespace-Zeichen findet), »Erstes Wort:
« plus der gefundenen
Übereinstimmung ausgeben. Wenn die Daten kein erstes Wort aufweisen (zum
Beispiel wenn es sich um einen leeren String handelt oder es kein Whitespace-
Zeichen in dem String gibt), wird nichts ausgegeben, da der Test falsch zurückliefert.
Wie bereits erwähnt, muss das ganze Muster und nicht nur der Teil in Klammern
stimmen, damit wahr zurückgeliefert werden kann.
Auf eines möchte ich Sie im Zusammenhang mit den Übereinstimmungsvariablen noch hinweisen: Die Werte dieser Variablen sind lokal zu ihrem Block, können nur gelesen werden und sind sehr kurzlebig. Schon beim nächsten Versuch, eine Übereinstimmung zu finden, verschwindet ihr alter Wert. Die Werte verschwinden auch, wenn ein Block endet. Mit anderen Worten, wenn Sie die Werte dieser Variablen ändern wollen, müssen Sie sie zur Sicherung in einer anderen Variablen oder in einer Liste ablegen. Übereinstimmungsvariablen dienen nur der vorübergehenden Speicherung.
Bis jetzt sind uns Muster nur in einem skalaren Kontext und dort vornehmlich in booleschen Tests begegnet. Für die Verwendung von Mustern in einem skalaren booleschen Kontext gibt es zwei Regeln:
/abc/
liefert 1 (wahr) zurück, wenn das Muster in dem gegebenen
String ($_
wenn es keine Variable ist oder =~
für alles andere) gefunden wurde,
ansonsten falsch. Sinnvoll vor allem in Bedingungen.
/g
nach dem Muster ermöglicht die Iteration durch den String. Das
Muster liefert für jede Übereinstimmung 1 (wahr) zurück und falsch am Ende des
Strings. Wird in while
-Schleifen eingesetzt.
In beiden Fälle füllen Klammern innerhalb des Musters die
Übereinstimmungsvariablen $1
, $2
, $3
und so weiter, und Sie können diese Werte
dann innerhalb des Bedingungs- oder Schleifenblocks oder bis zum nächsten
Mustervergleich verwenden.
Für Muster in einem Listenkontext gelten allerdings andere Regeln (Überraschung, Überraschung):
$1
, $2
, $3
und so weiter) - und natürlich auch die einzelnen Variablen. Gibt es in dem Muster
keine von Klammern eingeschlossenen Teilmuster, wird die Liste (1
)
zurückgegeben (das heißt eine Liste mit einem Element: der Zahl 1).
/g
nach dem Muster liefert eine Liste aller Teilmuster zurück, für die
im String eine Übereinstimmung gefunden wurde.
()
.
Da Muster in einem Listenkontext die gefundenen Übereinstimmungen als Liste zurückgeben, könnten Sie die Muster nutzen, um Ihre Daten in Elemente aufzusplitten. Hier eine Möglichkeit, einen Namen in die beiden Bestandteile Vor- und Nachname zu zerlegen:
($fn, $ln) = /^(\w+)\s+(\S+)$/
In Kapitel 2, »Mit Strings und Zahlen arbeiten«, hatten wir eine kurze Diskussion über die Wahrheit, und jetzt werden wir über Gier sprechen. Vielleicht kommen wir weiter hinten im Buch auch noch auf Gerechtigkeit und Neid zu sprechen.
Doch Humor beiseite, eine heikles Problem beim Extrahieren von Mustern betrifft die
Art und Weise, wie sich die quantifizierenden Metazeichen verhalten. Die Metazeichen
+
, *
, ?
und {}
werden auch gierige Metazeichen genannt, da sie im Falle einer
Übereinstimmung so viele Zeichen wie möglich in die Übereinstimmung mit
aufnehmen bis hin zum letzten Zeichen in der Zeile.
Normalerweise verhalten sich Muster so, dass sie, wenn sie gefundene Übereinstimmungen zurückliefern sollen (das heißt, wenn das Muster in einem Listenkontext verwendet wird), die erste Übereinstimmung zurückliefern. Betrachten wir beispielsweise folgenden Ausdruck:
@x = /(\d\d\d)/;
Angenommen die Daten in $_
sehen folgendermaßen aus:
3443 32 784 2344 123 78932
Das Array @x
endet als eine Liste von einem Element, den ersten drei Ziffern, in
diesem Fall 344
. Das Muster bricht immer nach dem ersten positiven Vergleich ab.
Die Quantifizierer *
, ?
und {}
unterliegen allerdings anderen Regeln. Nehmen wir als
Beispiel die Daten von oben und versuchen wir eine Übereinstimmung für folgendes
Muster zu finden:
/(\d*)/;
Da *
als »keines oder mehrere der vorhergehenden Zeichen« definiert ist, könnten Sie
davon ausgehen, dass der Vergleich mit dem Muster abgebrochen wird, sobald diese
Bedingung erfüllt ist, das heißt, sobald das erste passende Zeichen eingelesen wurde -
was in unserem Zahlenbeispiel von oben die Zahl 3 zurückliefern würde. Die Ziffer
entspricht zwar dem Muster, aber man darf nicht vergessen, dass *
ein gieriger
Quantifizierer ist, der bis an die Grenze des Möglichen alle Zeichen aufsaugt. Das
Ergebnis eines Vergleichs dieses Musters mit dem Zahlenstring lautet daher 3443. Der
*
-Quantifizierer geht Zahl um Zahl vor, bis er auf ein Leerzeichen trifft. Ein
Leerzeichen ist keine Zahl, und deshalb muss der Quantifizierer hier stoppen.
Hier sehen Sie ein noch schwierigeres Beispiel:
/'(.*)'/
Auf den ersten Blick scheint dieses Muster nach Zeichen in Anführungszeichen zu
suchen (und $1
mit diesen Zeichen zu füllen). Versuchen Sie jedoch einmal, dieses
Muster auf folgenden String anzuwenden:
"Sie sagte, 'Ich möchte diesen Käfer nicht essen,' und dann schlug sie mich."
Da die Sequenz .*
gierig ist, vergleicht sie alle Zeichen zwischen den einfachen
Anführungszeichen, liefert sie zurück (Ich möchte diesen Käfer nicht essen
) und
fährt dann damit fort, die restlichen Zeichen bis zum Ende der Zeile zu vergleichen. Da
die Sequenz dabei das Anführungszeichen nicht verglichen hat, geht Perl rückwärts
und versucht verschiedene Zeichen, bis es auf das Anführungszeichen stößt. Damit
erhalten Sie zwar das erwartete Ergebnis, doch Perl wird viel Zeit aufwenden müssen,
um zu dem Ergebnis zu gelangen. Es kann sogar noch schlimmer werden. Wenn Sie
nämlich mehrere Anführungszeichen im String haben, wird Perl das erste akzeptieren,
auf das es beim Rückwärtsgehen stößt. Nehmen wir folgenden String als Beispiel:
"'Ich verachte dich,' sagte sie und warf eine Kanne nach mir. 'Ich wünschte du wärest tot.'"
Wenn Sie versuchen, das gleiche Muster auf diesen String anzuwenden, wird $1
letztlich folgenden String enthalten:
Ich verachte dich,' sagte sie und warf eine Kanne nach mir. 'Ich wünschte du wärest tot.
Angenommen Sie wollten mit dem Muster ursprünglich nur nach dem Inhalt der
ersten Anführungszeichen suchen (nur die Wörter Ich verachte dich
), dann
entspricht dieses Ergebnis absolut nicht Ihren Erwartungen.
Quantifizierer scheinen auf den ersten Blick eine clevere Lösung zum Füllen einer Lücke zwischen zwei Mustern zu sein. Aufgrund ihres gierigen Verhaltens sind sie dafür jedoch häufig absolut ungeeignet, und Sie werden nur frustriert werden, wenn Sie versuchen, sie dafür einzusetzen. Die bessere Lösung - sowohl um sicherzustellen, dass nur gefunden wird, wonach gesucht wurde, als auch um Perl davon abzuhalten, unnötige Zeit mit dem Rückwärtsdurchlaufen eines Strings zu verbringen - ist die Verwendung von negierten Zeichenklassen anstatt der Quantifizierer. Betrachten Sie das Problem einmal nicht als »alle Zeichen zwischen dem öffnenden und dem schließenden Anführungszeichen«, sondern als »ein öffnendes Anführungszeichen, dann einige Zeichen, die kein Anführungszeichen sind, und dann ein schließendes Anführungszeichen«.
Wenn Sie diesen Gedanken in ein Muster umsetzen, sieht das folgendermaßen aus:
/"([^"]+)"/
Das sind zwar einige Zeichen mehr, und es ist insgesamt etwas schwieriger zu lesen, aber dafür liefert das Muster auch garantiert die Zeichen zwischen den Anführungszeichen zurück und frißt nicht gierig alle Zeichen nach dem schließenden Anführungszeichen, die dann ein Rückwärtssuchen erforderliche machen. Merken Sie sich diese Regel: Wenn Sie ein Muster zwischen Begrenzern vergleichen wollen, verwenden Sie am besten eine negierte Zeichenklasse mit dem schließenden Begrenzungszeichen innerhalb der ekkigen Klammern.
Ein zweiter, weniger effizienter Weg, das gierige Verhalten der Metazeichen +
, *
, ?
und {}
zu unterdrücken, besteht darin, besondere, nichtgierige (»faule«) Versionen
dieser Metazeichen zu verwenden: +?
, *?
, ??
und {}?
. Mehr dazu im Abschnitt
»Vertiefung«.
Reguläre Ausdrücke lassen sich nicht nur sehr gut zum Suchen eines Musters oder zum Ablegen von Übereinstimmungen in Listen oder Variablen oder ähnlichem nutzen, sondern vor allem auch für das Ersetzen der Vorkommen des Musters durch einen anderen String. Dies entspricht den Suchen&Ersetzen-Operationen Ihres bevorzugten Textverarbeitungssystems oder Editors plus der geballten Leistungsfähigkeit und Flexibilität der regulären Ausdrücke.
Die Syntax, mit der Sie nach etwas suchen, um es dann mit einem anderen Muster zu ersetzen, lautet:
s/muster/ersetzung/
In dieser Syntax ist muster
ein regulärer Ausdruck und ersetzung
der String, durch
den die gefundenen Vorkommen zu ersetzen sind. Fehlt die Angabe des Strings,
durch den ersetzt werden soll, werden die gefundenen Übereinstimmungen aus dem
Gesamtstring gelöscht. Ein Beispiel:
S/\s+/ / # ersetze ein oder mehrere Whitespaces durch ein Leerzeichen
Wie bei den normalen Mustern sucht und ersetzt diese Syntax standardmäßig in $_
.
Verwenden Sie den Operator =~
, wenn Sie in einem anderen String suchen wollen.
Die Suchen&Ersetzen-Syntax ersetzt nur die erste Übereinstimmung und liefert dann
1 zurück. Wird am Ende die Option /g
(steht für global) angegeben, ersetzt Perl alle
Vorkommen des Musters in dem String:
s/--/[md]/g # ersetze zwei Bindestriche durch den Gedankenstrich [md]
Sie können, wie bei normalen Mustern, am Ende auch die Option /i
verwenden, mit
der die Suche unabhängig von der Groß- und Kleinschreibung durchgeführt wird
(Vorsicht! Das bedeutet jedoch nicht, dass die Ersetzung sich dabei jeweils der
Schreibweise anpaßt):
s/a/b/gi; # ersetze [Aa] durch b, global
Lassen Sie sich nicht davon abhalten, innerhalb der Suchen&Ersetzen-Muster Klammern oder Übereinstimmungsvariablen zu verwenden. Man kann sie in vielfältiger Weise sinnvoll einsetzen:
s/^(\S+\b)/=$1=/g # setzt =-Zeichen um das erste Wort
s/^(\S+)(\s.*)(\S+)$/$3$2$1/ # tauscht das erste mit dem letzten Wort
Anhänger von
sed
werden vermutlich die Verwendung von/1
und/2
und so weiter in dem Ersetzungsteil der Suchen&Ersetzen-Operationen befürworten. Doch auch wenn dies in Perl funktioniert (vor allem weil Perl Ihnen diese Referenzen durch Variablen ersetzt), sollten Sie sich deren Verwendung allmählich abgewöhnen. Offiziell ist der Ersetzungsteil in dem Ausdrucks///
ein ganz normaler String in doppelten Anführungszeichen, und\1
bedeutet in diesem Kontext an sich etwas anderes.
Erinnern Sie sich noch an die split
-Funktion aus Kapitel 5, »Mit Hashes arbeiten«?
Wir haben mit split
die Vor- und Nachnamen in getrennten Listen ausgeben lassen:
($fn, $ln) = split(" ", $in);
Damals habe ich Ihnen erklärt, dass die Verwendung von split
mit einem
Leerzeichen in Anführungszeichen ein Sonderfall sei, der sich nur auf Daten
anwenden lasse, in denen die Felder durch Whitespace-Zeichen getrennt sind. Um
split
für Daten zu verwenden, die durch irgendein anderes Zeichen getrennt sind
oder die einer komplizierteren Verarbeitung bedürfen, um die Elemente zu finden,
müssen Sie split
um einen regulären Ausdruck für das zu vergleichende Muster
ergänzen:
($fn, $ln) = split(/\s+/, $in); # bei Whitespace zerlegen
@nums = split(//, $num); # zerlege 123 in (1,2,3)
@fields = split(/\s*,\s*/, $in); # zerlege durch Kommata getrennte Felder,
# mit oder ohne Whitespace um das Komma
Das erste Beispiel, das den String dort aufteilt, wo ein oder mehrere Leerzeichen
auftauchen, entspricht im Verhalten zum einem unserem Beispiel aus Kapitel 5 mit
dem Leerzeichen in Anführungszeichen und zum anderen einer Version, in der split
ohne irgendein Muster verwendet wird (die Syntax mit dem Leerzeichen in
Anführungszeichen lehnt sich an das Unix-Tool awk
an, das Strings auf gleiche Art
und Weise in Teilstrings zerlegt).
Sie können split
als drittes Argument auch eine Zahl mitgeben, die angibt, in wie
viele Teile die Daten zerlegt werden sollen:
($ln, $fn, $daten{$ln}) = split(/,/ $in, 3);
Dieser reguläre Ausdruck wäre beispielsweise für folgende Daten sehr nützlich:
Jones,Tom,braun,blau,64,32
Der split
-Befehl zerlegt die Daten beim Komma in drei Elemente: den Nachnamen,
den Vornamen und den Rest. Die Zuweisung wird den Nachnamen und den
Vornamen jeweils in Skalarvariablen ablegen und »den Rest« in einem Hash, das den
Nachnamen als Schlüssel verwendet.
Normalerweise enthalten die Teilstrings, die in der endgültigen Liste gespeichert werden, nichts, was dem Suchmuster entspricht. Wenn Sie jedoch im Muster Klammern verwenden, wird alles, was innerhalb dieser Klammern dem Suchmuster entspricht, auch in der endgültigen Liste erscheinen, wobei jede Entsprechung ein eigenes Listenelement darstellt. Angenommen Sie hätten folgenden String:
1:34:96:54:0
Wenn Sie diesen String an den Doppelpunkten zerlegen (mit dem Muster /:/
),
erhalten Sie eine Liste aller Elemente, die keine Doppelpunkte sind, also eine Liste
aller Zahlen. Mit dem Muster /(:)/
erhalten Sie folgende Liste, die sowohl Teilstrings
enthält, die nicht Bestandteil des Musters sind, als auch solche, die dem
Klammerinhalt des Musters entsprechen:
1, ':', 34, ':', 96, ':', 54, ':', 0
split
zusammen mit normalem Pattern Matching sollte es Ihnen ermöglichen, Ihre
Strings auf fast jede beliebige Weise zu zerlegen. Greifen Sie mit Mustern und
Rückbezügen auf die gewünschten Teile zu, und zerlegen Sie den String dann mit
split
und unter Angabe der Teile, die Sie nicht benötigen, in seine Bestandteile.
Jeder, der die Suchmuster korrekt einsetzt und das Format der Eingabedaten versteht,
wird keine Schwierigkeiten haben, Daten fast jeden Formats mit nur wenigen
Codezeilen zu verarbeiten. In einer Sprache wie C wäre dies nur mit einem wesentlich
größeren Aufwand zu bewerkstelligen.
Bis jetzt sind wir beim Pattern Matching von der Annahme ausgegangen, dass sich der
Vergleich nur auf einzelne Zeilen (Strings) beschränkt, die aus einer Datei oder von der
Tastatur aus eingelesen werden. Diese Annahme besagt, dass der String, der
durchsucht wird, keine Zeichen für Zeilenvorschub oder Wagenrücklauf enthält, und
dass die Anker für Anfang und Ende der Zeile sich auf den Anfang und das Ende des
Strings selbst beziehen. Für den while
-Code (<>
), den wir bisher geschrieben haben,
ist diese Annahme ziemlich sinnvoll.
Oft jedoch wollen Sie ein Muster über mehrere Zeilen hinweg vergleichen, vor allem, wenn Ihre Eingabedaten aus Sätzen oder Absätzen bestehen, deren Zeilengrenzen von der jeweiligen Textformatierung abhängen. Wenn Sie zum Beispiel alle Vorkommen der Bezeichnung »Exegetic Frobulator 5000« auf einer Webseite suchen, wollen Sie nicht nur die Bezeichnungen finden, die auf einer logischen Zeile stehen, sondern auch die, die sich über die Zeilengrenze hinweg in die nächste Zeile erstrecken.
Dazu müssen Sie in zwei Schritten vorgehen. Zuerst müssen Sie Ihre Eingaberoutinen dahingehend ändern, dass die gesamte Eingabe als ein String eingelesen und nicht Zeile für Zeile verarbeitet wird. Damit erhalten Sie einen extrem langen String mit Zeichen für »Neue Zeile« und »Wagenrücklauf«. Zweitens müssen Sie je nach Suchmuster, das Sie verwenden, Perl mitteilen, dass die Neue-Zeile-Zeichen anders zu handhaben sind.
Es gibt mehrere Möglichkeiten, Ihre gesamte Eingabe als einen String einzulesen. In
einem Listenkontext könnten Sie <>
verwenden:
@input = <>;
Die obige Zeile birgt allerdings ein großes Gefahrenpotential. Wenn nämlich Ihre
Eingabe sehr, sehr groß ist, könnte der gesamte zur Verfügung stehende Speicher
Ihres Systems in dem Versuch, all diese Daten einzulesen, aufgebraucht werden. Und
es gibt keine Möglichkeit, mittendrin abzubrechen. Etwas weniger aggressiv ist der
Ansatz, die vornehmlich in Absätzen vorliegenden Daten mit Hilfe der
Sondervariablen $/
einzulesen. Wenn Sie $/
auf einen Null-String setzen ($/ =
"";
),
dann liest Perl einen Absatz einschließlich der Neue-Zeile-Zeichen ein und hört auf,
sobald es auf zwei oder mehr Neue-Zeile-Zeichen in einer Reihe trifft (dabei gehen wir
davon aus, dass Ihre Eingabedaten eine oder mehrere Leerzeilen zwischen den
Absätzen enthalten):
$/ = "";
while (<>) { # liest einen Absatz keine, Zeile
# $_ enthält den gesamten Absatz und nicht nur eine Zeile
}
Der dritte Weg, mehrere Zeilen in einen einzigen String einzulesen, besteht darin,
verschachtelte while
-Schleifen zu verwenden und die eingelesenen Zeilen an einen
Eingabestring anzuhängen, bis ein bestimmter Begrenzer erreicht ist. In streng
codierten HTML-Dateien zum Beispiel endet ein Absatz mit einem </P>-
Tag, so dass
Sie in diesen Dateien die Eingabedaten bis zu diesem Punkt einlesen könnten:
while (<>) {
if (/(.*)<\/P>/) {
$in.=$1
} else {
$in.=$_
}
}
Haben Sie erst einmal Ihre mehrzeiligen Eingabedaten in einem String untergebracht
und entweder in $_
oder einer anderen Skalarvariablen gespeichert, können Sie damit
beginnen, diese Daten nach Mustern zu durchsuchen, die sich über mehrere Zeilen
erstrecken. Einiges gilt es jedoch zu beachten, wenn Sie eine Mustersuche bei Daten
mit eingebetteten Neue-Zeile-Zeichen durchführen wollen:
/s
umfaßt die sogenannten Whitespace-Zeichen einschließlich
des Neue-Zeile-Zeichens und des Zeilenvorschubs. Bei Verwendung dieser
Zeichenklasse haben Sie keine Probleme, Muster wie /George\s+Washington/
zu
finden, auch wenn diese nicht in einer Zeile stehen, sondern sich auf zwei Zeilen
verteilen.
^
und $
beziehen sich auf den Anfang oder das Ende eines
Strings, nicht jedoch auf eingebettete Neue-Zeile-Zeichen. Wenn diese
Ankerzeichen ihre Bedeutung auch in einem String beibehalten sollen, der aus
mehreren Zeilen besteht, müssen Sie die Option /m
verwenden.
.
) bricht in der Regel am Neue-Zeile-Zeichen ab! Dieses
Verhalten läßt sich jedoch mit der Option /s
ändern.
Der letzte Punkt hierbei ist am problematischsten. Betrachten wir folgendes Muster,
das den Quantifizierer .*
verwendet, um nach einer anfänglichen »Von:
«-Überschrift
den Rest einer Zeile auszulesen:
/Von: (.*)/
Dieses Muster sucht zuerst nach den Zeichen Von:
und füllt dann $1
mit dem Rest der
Zeile. Dies bereitet in der Regel bei einem String, der am Ende einer Zeile aufhört,
auch keinerlei Probleme. Wenn der String jedoch mehrere Zeilen lang ist, wird eine
Übereinstimmung nur bis zum ersten Neue-Zeile-Zeichen (\n
) erzielt, denn das
Punktzeichen berücksichtigt in der Regel die Zeichen für Neue Zeile.
Sie könnten das Problem umgehen, indem Sie das Muster ändern, so dass es nach
einem oder mehreren Wörtern oder Whitespace-Zeichen sucht, und den Punkt auf
diese Weise ganz vermeiden. Das wäre jedoch sehr arbeitsaufwendig. Was Sie hier
benötigen, ist die Option /s
am Ende Ihres Musters, die Perl anweist, das Metazeichen
Punkt um das Neue-Zeile-Zeichen (\n
) zu ergänzen. Die Option /s
hat keinen Einfluß
auf das Verhalten des restlichen Mustervergleichs - ^
und $
repräsentieren weiterhin
die Zeichen für Anfang und Ende des Strings.
Wenn Sie einen regulären Ausdruck mit den Zeichen ^
und $
verwenden, werden Sie
mehrzeilige Strings unter Umständen anders behandeln wollen als einzeilige Strings.
Standardmäßig beziehen sich ^
und $
auf den Anfang und das Ende des Strings, wobei
die Zeichen für eine neue Zeile ignoriert werden. Wenn Sie jedoch die Option /m
verwenden, bezieht sich ^
nicht nur auf den Anfang der des Strings, sondern auch auf
den Anfang einer Zeile (die Position direkt nach einem \n
), und $
betrifft sowohl das
Ende des Strings als auch das Ende der Zeile (die Position direkt vor dem \n
). Mit
anderen Worten, wenn Ihr String vier Textzeilen enthält, wird das Zeichen ^
viermal
zu einer Übereinstimmung führen. Das gleiche gilt für $
. Dazu ein Beispiel:
while (/^(\w)/mg) {
print "$1\n";
}
Diese while
-Schleife gibt das erste Wort jeder Zeile in $_
aus, unabhängig davon, ob
die Eingabe eine Zeile oder mehrere enthält.
Wenn Sie jedoch die Option /m
verwenden und ausnahmsweise den Anfang oder das
Ende des Strings abfragen wollen, lassen sich ^
und $
dafür nicht länger verwenden.
Aber keine Angst, auch dafür hat Perl eine Lösung parat: Mit \A
und \Z
beziehen Sie
sich auf den Anfang und das Ende des Strings, unabhängig vom Status von /m
.
Sie können problemlos die Optionen /s
und /m
zusammen verwenden. Sie müssen
sich lediglich merken, dass /s
das Verhalten des Punktes beeinflußt und /m
das
Verhalten von ^
und $
. Darüber hinaus stellen eingebettete Neue-Zeile-Zeichen in
Strings kein Problem für Pattern Matching dar.
Im Verlaufe dieses Kapitels habe ich Ihnen diverse Optionen vorgestellt, die Sie zusammen mit den Mustern verwenden können, sowie eine Reihe von besonderen Escape-Zeichen, die innerhalb von Mustern verwendet werden können.
In Tabelle 10.1 finden Sie die Optionen, die Sie an das Ende eines Musters (m//
oder
nur //
) setzen können, aber auch jene, die zusammen mit dem Ersetzungsausdruck
gültig sind (s///
).
Ich habe in diesem Kapitel nicht alle der hier aufgeführten Optionen besprochen. Einige werden Ihnen noch im Abschnitt »Vertiefung« vorgestellt. Falls Sie selbst nachforschen wollen, welche Möglichkeiten Ihnen bestimmte Optionen im Detail bieten, sollten Sie dazu in die perlre-Manpage schauen.
erweitert die regulären Ausdrücke (kann Kommentare und Whitespace-Zeichen mit einschließen) | |
bewertet Ersetzung als Perl-Ausdruck (nur Substitution mit s///) |
Tabelle 10.1: Optionen zum Pattern Matching und Suchen&Ersetzen
Tabelle 10.2 enthält die besonderen Escape-Zeichen, die in den regulären Ausdrücken
zusätzlich zu den normalen String-Escape-Zeichen (\t
, \n
, Backslash, der Metazeichen
als gewöhnliches Zeichen interpretiert) verwendet werden können.
Beenden möchte ich meine bisherigen Ausführungen mit einem Beispiel für einen
ziemlich umfangreichen regulären Ausdruck (eigentlich sind es zwei Ausdrücke)
innerhalb eines Perl-Skripts. Dieses Skript erhält als Eingabe eine HTML-Datei, geht
die Datei durch und sucht nach eingebetteten Grafiken (mit Hilfe des <IMG
>-Tags in
HTML). Danach gibt es eine Liste der Grafiken in der Seite aus sowie eine Liste der
verschiedenen Attribute zu der jeweiligen Grafik (ihre Position, Breite oder Höhe,
Textalternative etc.). Die Ausgabe des Skripts wird in etwa so aussehen:
---------------
Grafik: title.gif
HSPACE: 4
VPSACE: 4
ALT: *
---------------
Grafik: smbullet.gif
ALT: *
---------------
Grafik: rib_bar_wh.gif
BORDER: 0
HSPACE: 4
WIDTH; 50
HEIGHT: 50
ALT: --
Falls Sie mit HTML nicht vertraut sind, sehen Sie hier ein Beispiel für das <IMG
>-Tag,
das an einer beliebigen Stelle in einer HTML-Datei eingefügt werden kann:
<IMG SRC="imgfile.gif" WIDTH=50 HEIGHT=75 ALT="Pinguine">
Dieses Tag weist einige knifflige Besonderheiten auf, die Ihre Aufgabe schwieriger
machen, als sie auf den ersten Blick scheint. Zum einen kann das Tag selbst in Groß-
oder in Kleinbuchstaben vorkommen, und es kann sich über mehr als eine Zeile
erstrecken. Die Attribute (die Schlüssel/Werte-Paare nach dem IMG
-Teil) können
ebenfalls groß oder klein geschrieben sein, können Leerzeichen links und rechts des
Gleichheitszeichen haben und in Anführungszeichen stehen oder nicht. Die Werte
können Leerzeichen enthalten (müssen dann allerdings von Anführungszeichen
umschlossen sein). Es ist nur ein Attribut erforderlich, das SRC
-Attribut, aber es gibt
weitere Attribute, die in beliebiger Reihenfolge erscheinen können.
All diese Möglichkeiten sind dafür verantwortlich, dass der reguläre Ausdruck
wesentlich komplexer wird, als wenn damit nur der Inhalt zwischen dem öffnenden
und dem schließenden Tag aufgenommen werden sollte. Um genau zu sein, habe ich
in diesem Skript die Aufgabe auf zwei reguläre Ausdrücke verteilt: Einer sucht das IMG
-
Tag in der Datei und zieht es heraus, und ein anderer extrahiert und parst die
einzelnen Attribute.
Listing 10.1 enthält den Code zu diesem Skript. Versuchen Sie schon einmal, es durchzugehen, um ein Gefühl für den Aufbau zu bekommen. Aber sorgen Sie sich nicht zu sehr, wenn Sie die Muster noch nicht vollständig verstehen.
Listing 10.1: Das Skript img.pl
1: #!/usr/bin/perl -w
2:
3: $/ = ""; # Absatzeingabe-Modus
4: $raw = ""; # rohe Attribute
5: %atts = (); # Attribute
6:
7: while (<>) {
8: while (/<IMG\s+([^>]+)>/ig ) {
9: $raw = $1;
10: while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
11: if (defined $3) {
12: $atts{ uc($1) } = $3;
13: } else { $atts{ uc($1)} = $2; }
14: }
15: if ($raw =~ /ISMAP/i) {
16: $atts{'ISMAP'}= "Yes";
17: }
18:
19: print '-' x 15;
20: print "\nGrafik: $atts{'SRC'}\n";
21: foreach $key ("WIDTH", "HEIGHT",
22: "BORDER", "VSPACE", "HSPACE",
23: "ALIGN", "ALT", "LOWSRC", "ISMAP") {
24: if (exists($atts{$key})) {
25: $atts{$key} =~ s/[\s]*\n/ /g;
26: print " $key: $atts{$key}\n";
27: }
28: }
29: %atts = ();
30: }
31: }
Dieses Skript besteht aus zwei Teilen: einem Abschnitt, der die Daten aus der Eingabe herauszieht, und einem Abschnitt, der in Protokollform ausgibt, was gefunden wurde.
Der erste Teil verwendet eine Reihe von verschachtelten while
-Schleifen, um die
HTML-Datei durchzugehen: Zeile 7 durchläuft die gesamte Eingabe, Zeile 8 sucht in
der Eingabe nach jedem Vorkommen des Image-Tags, und Zeile 10 durchläuft jedes
Attribut im <IMG
>-Tag und speichert es in einem Hash namens %atts
mit dem
Attributnamen als Schlüssel.
Nachdem der Hash %atts
gefüllt ist, müssen wir nur noch die Werte ausgeben. Da ich
möchte, dass sie in einer speziellen Reihenfolge ausgegeben werden, habe ich in Zeile
21 mit einer foreach
-Schleife angegeben, in welcher Reihenfolge die Schlüssel
auszugeben sind.
Unser Hauptaugenmerk in diesem Skript soll aber auf den regulären Ausdrücken in den Zeilen 8 und 10 liegen. Betrachten wir also diese zwei Muster im Detail. Zeile 8 lautet:
while (/<IMG\s+([^>]+)>/ig) {
Gehen wir diesen regulären Ausdruck zeichenweise durch: Zuerst suchen wir nach
den Zeichen <IMG
(der Öffnungsteil des Tags) und dann nach einem oder mehreren
Whitespace-Zeichen, gefolgt von einem oder mehreren Zeichen, die nicht >
sind. Das
Muster selbst wird mit dem eigentlichen >
-Zeichen, das das Ende des Tags signalisiert,
beendet.
Beachten Sie die Klammern um den Teil, der die »ein oder mehrere Zeichen, die nicht
>
sind« repräsentiert - genau dieser Teil interessiert uns besonders, da er die Attribute
für die Grafik enthält. Dieser Teil des Musters wird herausgezogen und gespeichert, so
dass er später in dem Rumpf der Schleife verwendet werden kann.
Eine Bemerkung wert sind auch die Optionen am Ende des Musters: /i
steht für eine
Suche, die Groß- und Kleinschreibung unberücksichtigt läßt (gesucht wird sowohl
<img...>
als auch <IMG...>,
und /g
steht für eine globale Suche (wir durchlaufen für
jedes <IMG
, auf das wir stoßen, einmal die while
-Schleife innerhalb der Schleife).
Beachten Sie, dass wir /s
oder /m
eigentlich nicht explizit angeben müssen, um die
Suche über Zeilengrenzen hinweg auszuführen - HTML erfordert Zeilenumbrüche nur
für Whitespace, und unser Muster verwendet /s
für alle Whitespace-Zeichen, so dass
wir in dieser Hinsicht auf der sicheren Seite sind. Wir werden kein Problem mit Tags
haben, die sich über mehrere Zeilen erstrecken.
Im Rumpf der Schleife können wir die Attribute-Liste in der Variablen $raw
(Zeile 9)
speichern. Wir sind dazu gezwungen, da die Werte von $1
, $2
, $3
und so weiter alle
nur kurzlebig sind - sie werden beim nächsten Mustervergleich zurückgesetzt. Damit
kommen wir zu Zeile 10 und dem folgenden wirklich fast undurchschaubaren
regulären Ausdruck:
while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
Dieser reguläre Ausdruck läßt sich in vier wichtige Teile gliedern, die ich in Abbildung 10.1 veranschaulichen möchte:
Abbildung 10.1: Die Teile des regulären Ausdrucks in img.pl
Die Teile lassen sich auch wie folgt beschreiben:
$1
.
ALT="ein alternativer
Text"
).
HEIGHT=100
).
Beachten Sie die Position der Klammern für die Werte, und vergegenwärtigen Sie sich
die Regel für Übereinstimmungsvariablen: Die Zahlen werden auf der Basis der
öffnenden Klammer vergeben. Deshalb repräsentiert $2
den kompletten Wert,
während $3
der Wert minus der Angabe in Anführungszeichen ist - falls der Wert
überhaupt Anführungszeichen hatte.
In den Zeilen 11 bis 14 werden beide Fälle berücksichtigt, indem wir testen, ob $3
gesetzt ist. Wenn ja, hat unser Wert Anführungszeichen, und der Teil, der nicht in den
Anführungszeichen steht, wird im Hash %atts
gespeichert. Ist $3
nicht definiert, steht
unser Wert nicht in Anführungszeichen, und wir können statt dessen $2
in %atts
ablegen. Außerdem möchte ich Sie darauf hinweisen, dass wir die Funktion uc
verwendet haben, um unsere Attributnamen in Großbuchstaben zu wandeln, bevor sie
gespeichert werden.
Die Zeilen 15 bis 17 behandeln einen Sonderfall des <IMG>
-Tags: Das Attribut ISMAP
übernimmt keinen Wert. Es zeigt lediglich an, ob es sich bei der Grafik um eine
Imagemap handelt. (Eine Imagemap ist eine anklickbare Grafik, das heißt, Sie können
verschiedene Bereiche der Grafik anklicken und lösen damit unterschiedliche
Aktionen aus.) Diese Art von anklickbarer Grafik wird in HTML normalerweise nicht
mehr verwendet (das Tag wurde durch ein anderes Tag ersetzt), aber der
Vollständigkeit halber habe ich es mit aufgenommen. Wir mußten dies zu einem
Sonderfall machen, da es nicht durch den Ausdruck in Zeile 10 abgefangen wird.
Nach Ausführung all dieser Schleifen und Muster haben wir einen Hash vorliegen, in
dem alle gefundenen Attribute des <IMG>
-Tags gespeichert sind. Uns bleibt nur noch,
diese Werte auszugeben. Die Schleife in Zeile 21 durchläuft alle möglichen Werte für
das <IMG>
-Tag in der Reihenfolge, in der sie ausgegeben werden. Jedes <IMG>
, das wir
in der Datei finden, kann potentiell eine Teilmenge dieser Attribute aufweisen, die bis
auf SRC
alle optional sind. Deshalb müssen wir in Zeile 24 einen Test durchführen, um
sicherzustellen, dass das Attribut auch tatsächlich existiert, bevor es ausgegeben wird.
Die Funktion exists
testet einen Hash, um festzustellen, ob ein Schlüssel existiert,
und liefert wahr oder falsch zurück.
Ungewöhnlich ist auch die Zeile 25, in der wir eine schnelle Suchen&Ersetzen- Operation auf dem Wert durchführen. Damit wollen wir die Fälle abfangen, in denen sich das Attribut auf mehrere Zeilen verteilt - der Wert also ein Neue-Zeile-Zeichen enthält, das in der endgültigen Tabelle nicht mit ausgegeben werden soll. Dieser kleine reguläre Ausdruck sucht optionale Whitespace-Zeichen gefolgt von einem Neue-Zeile- Zeichen und ersetzt sie durch ein einfaches Leerzeichen.
Zum Schluß in Zeile 29 löschen wir den Attribut-Hash für die nächste Runde und das
nächste <IMG>
-Tag.
Dieses Skript ist zwar kurz, zeigt aber vorbildlich, für welche Art Aufgaben sich Perl besonders gut eignet: komplizierte Muster in Texten zu suchen und diese dann in anspruchsvollen Protokollen auszugeben. Wenn Sie die gleiche Aufgabe in C lösen wollten, würden Sie mit Sicherheit mehr als 30 Zeilen dafür benötigen.
Je nach der Komplexität Ihrer Daten oder der Aufgabe, die damit zu bewerkstelligen ist, kann die Formulierung eines regulären Ausdrucks sehr einfach sein oder mehrerer Überarbeitungen bedürfen. Hier möchte ich Ihnen einige Tips geben, die Ihnen helfen sollen, Suchmuster zu verwenden:
split
nicht. Einige Aufgaben lassen sich besser mit split
lösen
(alles außer einem Muster entfernen) als mit einfachen Mustervergleichen. Und
umgekehrt.
.*
gierig sind, das heißt alle Zeichen bis zum Ende
der Zeile schlucken und Ihnen das Leben schwer machen können. Ganz
abgesehen davon, dass sie auch für Perl selbst einen zusätzlichen Arbeitsaufwand
bedeuten. Vermeiden Sie diese Konstrukte überall dort, wo sich eine negierte
Zeichenklasse besser einsetzen läßt.
*
und ?
für »kein oder mehrere« beziehungsweise »kein
oder ein« Zeichen stehen. Dies bedeutet, dass das Suchmuster auch dann
erfolgreich sein kann, wenn das Zeichen gar nicht vorkommt. Wenn Ihr Muster
erfordert, dass ein Zeichen mindestens einmal vorhanden ist, verwenden Sie +
anstelle von * oder ?.
|
) kann für hochkomplexe Muster, die mehrere komplexe alternative Fälle
aufweisen, sehr praktisch sein.
Reguläre Ausdrücke gehören zu den Themen, mit denen man ein ganzes Buch füllen und trotzdem noch Fragen offenlassen kann. In den Kapiteln von gestern und heute habe ich Ihnen die Grundlagen vermittelt, wie man reguläre Ausdrücke aufbaut und in eigenen Programmen verwendet. Es gibt jedoch noch viele Punkte, die ich nicht angesprochen habe, unter anderem eine Unmenge von Metazeichen und Perl- spezifischen regulären Ausdrücken. In diesem Abschnitt möchte ich Ihnen einen Überblick über einige davon geben.
Weitere Informationen zu anderen Aspekten der regulären Ausdrücke in Perl finden Sie in der perlre-Manpage, die ziemlich aufschlußreich ist. Wenn Sie feststellen, dass Ihnen die Arbeit mit regulären Ausdrücken großen Spaß macht und Sie des Englischen mächtig sind, sollten Sie die Anschaffung des Buches »Reguläre Ausdrücke« (Jeffrey Friedl, O'Reilly Verlag) in Erwägung ziehen. Dieses Buch beschreibt erstaunlich detailliert reguläre Ausdrücke aller Art - sowohl von Perl als auch von anderen Sprachen.
Mit den Metazeichen, die ich gestern und heute beschrieben habe, habe ich Ihnen den größten Teil der elementaren Zeichen vorgestellt, die in den meisten regulären Ausdrücken (nicht nur bei Perl) Verwendung finden. Perl umfaßt eine Reihe von zusätzlichen Metazeichen, die noch andere Möglichkeiten bieten, komplexe Muster zu erstellen (oder die gefundenen Muster zu verarbeiten).
Als erstes möchte ich hier die nichtgierigen Versionen der Quantifizierer *
, +
, ?
und {}
nennen. Wie Sie in diesem Kapitel gelernt haben, gehören die Quantifizierer
eigentlich zu den gierigen Metazeichen. Sie vergleichen alle Zeichen und damit weit
mehr, als von Ihnen erwartet - was Ihnen manchmal zum Nachteil gereichen kann,
wenn Sie herausfinden wollen, wonach das Muster eigentlich sucht. Zu diesen
Quantifizierern bietet Perl Ihnen nun als Gegenstücke die nichtgierigen Quantifizierer
(manchmal auch faule Quantifizierer genannt): *?
, +?
, ??
und {}?
. Diese Quantifizierer
vergleichen die Mindestanzahl der Zeichen, die für den Mustervergleich nötig sind,
während die regulären Quantifizierer die maximale Zahl der Zeichen vergleicht. Dies
kann in manchen Situationen nützlich sein; Sie sollten jedoch nicht vergessen, wenn
möglich auch mit negierten Zeichenklassen zu arbeiten. Die faulen Quantifizierer sind
nicht so effizient wie eine negierte Zeichenklasse und führen oft zu unerwarteten
Ergebnissen.
Das Konstrukt (?:muster
) ist eine Variante zu den Klammern, mit denen Muster
zusammengefaßt und die Ergebnisse in den Übereinstimmungsvariablen $1
, $2
, $3
etc.
gespeichert werden. Wenn Sie Klammern verwenden, um einen Ausdruck als Gruppe
zusammenzufassen, wird das Ergebnis automatisch auch gesichert, ob Sie wollen oder
nicht. Mit dem Konstrukt (?:muster
) hingegen wird der Ausdruck als Einheit
zusammengefaßt und ausgewertet, das Ergebnis jedoch nicht gespeichert. Dies ist
effizienter als die Verwendung der normalen Klammern, wenn das Ergebnis keine
Rolle spielt.
Mit dem Konstrukt (?o
) können Sie die Optionen für das Pattern Matching innerhalb
des Musters selbst verwenden. So können Sie damit zum Beispiel bestimmte Teile des
Ausdrucks von der Unterscheidung der Groß- und Kleinschreibung befreien. Das o
in
dem Konstrukt kann eine beliebige gültige Option für den Mustervergleich sein.
Die Voraussschau ist eine Besonderheit der regulären Ausdrücke in Perl, die es Perl
erlaubt, den String kurz zu überfliegen und zu schauen, ob es nicht irgendwo eine
Übereinstimmung mit dem Muster gibt, ohne dass dabei die Position im String
geändert oder irgend etwas den geklammerten Teilen des Musters hinzugefügt wird.
Das entspricht in etwa der Aussage: »Wenn der nächste Teil dieses Musters X enthält,
dann stimmt dieser Teil überein«, ohne dass dabei die Position verlassen wird. Mit
(?=muster
) erzeugen Sie ein positives Muster für die Vorausschau (wenn muster
im
weiteren Verlauf des Strings eine Übereinstimmung hat, dann hat der
vorangegangene Teil des Musters auch eine Übereinstimmung). Das Gegenteil davon
ist ein negatives Muster (?!muster
). Es stimmt nur dann überein, wenn zu dem muster
keine Übereinstimmung gefunden wird.
Zusätzlich zu den Übereinstimmungsvariablen $1
, $2
und so weiter gibt es in Perl
noch die Variablen $'
, $&
und $´
, die einen Kontext für die Textfundstellen des
Musters bereithalten. $'
bezieht sich dabei auf den Text bis zur Fundstelle, $&
steht für
den gefundenen Text und $´
für den Text nach der Fundstelle (beachten Sie den
Apostroph, der nicht mit dem einfachen Anführungszeichen '
identisch ist). Im
Gegensatz zu den kurzlebigen Übereinstimmungsvariablen halten diese Variablen ihre
Werte bis zum nächsten erfolgreichen Vergleich fest - unabhängig davon, ob der
ursprüngliche String sich geändert hat oder nicht. All diese Variablen zehren jedoch
enorm an der Leistung, deshalb sollten Sie sie wenn möglich vermeiden.
Die Variable $+
gibt die höchste Zahl der definierten Übereinstimmungsvariablen an.
Wurden zum Beispiel $1
und $2,
jedoch nicht $3,
mit Werten gefüllt, wird $+
auf 2
gesetzt.
Im Verlaufe dieses Kapitels haben Sie den größten Teil der Optionen für reguläre
Ausdrücke in Perl kennengelernt (sowohl m//
als auch s///
). Zwei sind jedoch noch
unerwähnt geblieben: /x
für erweiterte reguläre Ausdrücke und /o
, um wiederholtes
Kompilieren des gleichen regulären Ausdrucks zu vermeiden.
Mit der Option /x
können Sie reguläre Ausdrücke zur besseren Lesbarkeit mit
Whitespace-Zeichen und Kommentaren versehen. Wenn Sie normalerweise
Leerzeichen in einem Muster verwenden, werden diese Leerzeichen als Teil des
Musters selbst betrachtet. Mit der Option /x
werden nicht nur alle Leerzeichen und
Neue-Zeile-Zeichen ignoriert, es ist damit auch möglich, Kommentare zu den
einzelnen Zeilen des regulären Ausdrucks einzubauen. So könnten wir zum Beispiel
den regulären Ausdruck in unserem Skript img.pl, der folgendermaßen aussah:
while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
while ($raw =~ /([^ =]+) # sucht den Attributnamen
\s*=\s* # sucht Gleichheitszeichen mit und ohne
# Whitespaces
("([^"]+)"| # sucht und extrahiert Werte in
# Anführungszeichen
[^\s]+\s*) # oder sucht Werte nicht in Anführungszeichen
/igx) {
Die Verwendung von erweiterten regulären Ausdrücken kann eine große Hilfe sein und die Lesbarkeit eines regulären Ausdrucks verbessern.
Schließlich gibt es noch die Option /o
, die dazu dient, Perl beim Kompilieren und
Einlesen eines regulären Ausdrucks, der über eine Skalarvariable interpoliert wurde, zu
optimieren. Betrachten wir folgendes Codefragment:
while (<>) {
if (/$muster/) {
...
}
}
In diesem Fragment wird das in $muster
gespeicherte Muster interpoliert und in ein
richtiges Muster kompiliert, das Perl versteht. Das Problem dabei ist jedoch, dass
dieses Muster sich innerhalb einer Schleife befindet und sich deshalb der Prozeß bei
jedem Durchlauf der Schleife wiederholt. Mit der Angabe von /o
am Ende des Musters
teilen Sie Perl mit, dass sich das Muster nicht ändert und deshalb nur einmal
kompiliert werden muss, um dann einfach wiederverwendet zu werden:
if (/$muster/o) { # nur einmal kompilieren
Informationen zu diesen Metazeichen, Variablen und Optionen finden Sie auch in der Hilfsdokumentation perlre-Manpage.
Im heutigen Kapitel haben wir, aufbauend auf den Grundlagen von gestern, das Thema der regulären Ausdrücke vertieft. Wir haben angesprochen, wie man Fundstellen aus einer Pattern-Matching-Operation mit Hilfe von Klammern herauszieht und wie man durch die Verwendung von Rückbezügen und Übereinstimmungsvariablen gefundene Vorkommen speichert, um später auf sie zuzugreifen.
Im Rahmen dieser Diskussion haben Sie Pattern Matching in verschiedenen
Kontexten kennengelernt (skalare Kontexte liefern wahr oder falsch zurück,
Listenkontexte liefern Listen der Übereinstimmungen zurück). Außerdem habe ich
Ihnen etwas über das gierige Verhalten der quantifizierenden Metazeichen und die
erweiterten Möglichkeiten der split
-Funktion erzählt. Wenn Sie die beiden Kapitel zu
den regulären Ausdrücken durchgearbeitet haben, sollten Sie genug über reguläre
Ausdrücke wissen, um so ziemlich jedes Muster mit einem beliebigen Satz an Daten zu
vergleichen.
Frage:
Ich habe hier einige Codezeilen, die in zwei Schritten versuchen, etwas aus einem
String herauszuziehen. Das erste Muster legt einen Teilstring in $1
ab, und dann
durchsucht das zweite Muster $1
nach einem weiteren Muster. Das zweite Muster
führt nie zu einer Übereinstimmung, und die Ausgabe von $1
zeigt, dass die
Variable leer ist. Wenn diese Variable aber leer ist, so hätte das zweite Muster
eigentlich gar nicht erst verglichen werden sollen. Was läuft hier falsch?
Antwort:
Das klingt, als wenn Sie ungefähr folgendes versucht hätten:
if ($string =~ /ein langes Muster mit einem {Teilmuster} darin/) {
if ($1 =~ /ein zweites Muster/) {
# verarbeitet zweites Muster
}
}
Leider geht das so nicht. Die Variable
$1
(oder jede andere Variable) ist unglaublich kurzlebig. Jedesmal, wenn Sie einen regulären Ausdruck verwenden, setzt Perl alle Übereinstimmungsvariablen zurück. Bei diesem speziellen Beispiel hier gibt es in der ersten Zeile eine Übereinstimmung, und die Variable$1
wird mit dem Inhalt der Klammern gefüllt. Sobald Sie jedoch einen neuen Mustervergleich durchführen (in der zweiten Zeile), verschwindet der Wert von$1
, was konkret bedeutet, dass das zweite Muster nie zu einer Übereinstimmung führen kann. Das Geheimnis liegt darin, sicherzustellen, dass die Werte aller Übereinstimmungsvariablen irgendwo anders abgelegt werden, wenn Sie wiederverwendet werden sollen. In diesem besonderen Fall reicht es aus, eine temporäre Variable hinzuzufügen und diese dann zu durchsuchen:
if ($string =~ /ein langes Muster mit einem {Teilmuster} darin/) {
$tmp = $1;
if ($tmp =~ /ein zweites Muster/) {
# verarbeitet zweites Muster
}
}
Frage:
Ich habe einige Skripts gesehen, die eine $*
-Variable auf 1 gesetzt haben, um
einen Mustervergleich über mehrere Zeilen durchzuführen - wozu sie die Option /
m
verwenden. Was bedeutet $*
, und kann ich es auch verwenden?
Antwort:
In früheren Versionen von Perl hat man $*
gesetzt, um Perl anzuweisen, die
Bedeutung von ^
und $
zu ändern. In aktuellen Perl-Versionen jedoch sollten
Sie statt dessen die Option /m
verwenden. $*
wurde lediglich aus Gründen der
Rückwärtskompatibilität beibehalten.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie zur Lektion des nächsten Tages übergehen.
$_
enthält 123 kazoo kazoo 456
. Wie lauten die Ergebnisse der
folgenden Ausdrücke?
@matches = /(\b[^\d]+)\b/g;
@matches = /\b[^\d]+\b/;
s/\d{3}/xxx/;
s/\d{3}/xxx/g;
$matches = s/\d{3}/xxx/g;
if (/\d+(.*)\d+/) { print $1;}
@matches = split(/z/);
@matches = split(" ",$_ 3);
+
und *
unterbinden?
while (<>) {
$input =~ /pat\s/path /;
}
@matches = /\b[^\d]+\b/;
HTML (HyperText Markup Language)
ICBM (InterContinental Ballistic Missile)
EEPROM (Electrically-erasable programmable read-only memory)
SCUBA (self-contained underwater breathing apparatus)
FAQ (Frequently Asked Questions)
img.pl, so dass es Links anstelle von Grafiken
herauszieht und protokolliert. TIP: Links haben ungefähr folgendes Format:
<A HREF="url_des_Links">Text des Links</A>
NAME
, REL
, REV
, TARGET
und TITLE
.
Hier die Antworten auf die Workshop-Fragen aus dem vorigen Abschnitt.
b. (
1
). Ein Muster in einem Listenkontext ohne Teile des Musters in Klammern oder mit der Option/g
liefert im Falle einer Übereinstimmung (1
) zurück
e. Die Variable
$matches
wird auf 2 gesetzt (die Anzahl der vorgenommenen Änderungen)
f. »
kazoo kazoo 45
« (behalten Sie die gierigen Quantifizierer im Auge)
.
) oder durch Einsatz negierter Zeichenklassen.
Der zweite Weg führt über die nichtgierigen Versionen der Quantifizierer (+?
und
*?
).
a. Die Option
/g
bedeutet »global«. Das Muster wird im ganzen String gesucht (die Suche wird nicht nach dem ersten Vorkommen abgebrochen). Je nach Kontext ist der weitere Verlauf unterschiedlich.
b. Die Option
/i
unterdrückt die Unterscheidung nach Groß- und Kleinschreibung bei der Suche.
c. Die Option
/o
bedeutet: »Kompiliere dieses Muster nur einmal«. Sie ist besonders nützlich zum Optimieren von Mustern mit eingebetteten Variablen.
d. Die Option
/s
ermöglicht es dem Punktzeichen (.
), das Neue-Zeile-Zeichen (\n
) zu übergehen.
s
. Die korrekte Version hierfür sieht folgendermaßen aus:
while (<>) {
$input =~ s/pat\s/path /;
}
1
),
wenn das Muster zu einer Übereinstimmung führt. Um eine Liste der
Übereinstimmungen zu sichern, müssen Sie irgendwo im Muster Klammern
setzen.
#!/usr/bin/perl -w
#
# Sucht doppelte Wörter ohne Rücksicht auf Zeilenumbrüche
# diese Version sucht Groß- und Kleinbuchstaben, aber berücksichtigt
# keine Zeichensetzung oder mehr als zwei Vorkommen des gleichen Wortes.
$/ = ""; # Absatzeingabe-Modus
while (<>) {
s/\b(\w+)\s+\1\b/$1/ig;
print;
}
#!/usr/bin/perl -w
%acs = (
"HTML" => "HyperText Markup Language",
"ICBM" => "InterContinental Ballistic Missile",
"EEPROM" => "Electrically-erasable programmable read-only memory",
"SCUBA" => "self-contained underwater breathing apparatus",
"FAQ" => "Frequently Asked Questions",
);
while (<>) {
foreach $key (keys %acs) {
s/$key/$key ($acs{$key})/gi;
}
print;
}
#!/usr/bin/perl -w
# sucht und extrahiert Links
# Lässt Link-Text mit eingebettetem HTML unberücksichtigt
$/ = ""; # Absatzeingabe-Modus
$raw = ""; # rohe Attribute
$linktext = ""; # Link-Text
%atts = (); # Attribute
while (<>) {
while (/<A\s+([^>]+)>([^<]+)<\/A>/ig) {
$raw = $1;
$linktext = $2;
$linktext =~ s/[\s]*\n/ /g;
while ($raw =~ /([^\s=]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
if (defined $3) {
$atts{ uc($1) } = $3;
} else { $atts{ uc($1)} = $2; }
}
print '-' x 15;
print "\nLink-Text: $linktext\n";
foreach $key ("HREF", "NAME", "TITLE",
"REL", "REV", "TARGET") {
if (exists($atts{$key})) {
$atts{$key} =~ s/[\s]*\n/ /g;
print " $key: $atts{$key}\n";
}
}
%atts = ();
}
}